Прогнозирование влажности листьев
Нучно-исследовательская работа
Цель работы: Проверить предположение о возможности разработки модели прогнозирования на основе представленных данных, используя методы анализа данных и машинного обучения.
Влажность листьев - это метеоролигический параметр, который описывает количество росы и осадков, оставшихся на поверхности. Он используется для мониторинга влажности листьев в сельскохозяйственных целях,таких как борьба с грибками и болезнями, для контроля ирригационных систем, а также для обнаружения тумана и условий росы, а также для раннего обнаружения дождя.
Для измерения влажности листьев существуют датчики, например от австрийской компании Pessl Instruments GmbH (https://metos.at/ru/portfolio/leaf-wetness/), изображенный на рисунке 1.
Для задачи анализа данных и машинного обучения представлен набор структурированных данных в виде .excel-файла содержащий измерения данного датчика. Кроме того в файле содержаться результаты прочих метерологических измерений, которые собирались синхронно, с фиксацией влажности листьев в том же месте, где располагался датчик.
В процессе проведения исследования необходимо ответить на следующие вопросы:
Результат анализа данных и моделирования представить в виде .ipynb-файла, с подробным описанием процесса.
import math
import os
import re
from datetime import date, datetime
from typing import Any
from IPython.display import HTML, IFrame, YouTubeVideo, clear_output, display
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
!pip install pandas==1.5.3
!pip install matplotlib==3.6.3
!pip install openpyxl==3.1.0
!pip install scikit-learn==1.2.1
!pip install july==0.1.3
!pip install plotly=5.13.0
!pip install altair==4.2.2
!pip install seaborn==0.12.2
!pip install tensorflow-cpu==2.11.0
!pip install xgboost==1.7.3
!pip install ipywidgets==8.0.4
!pip install optuna==3.1.0
clear_output()
import warnings
import altair as alt
import july
import matplotlib.pyplot as plt
import numpy as np
import optuna
import pandas as pd
import plotly
import plotly.express as px
import plotly.io as pio
import seaborn as sns
import tensorflow as tf
import xgboost
from july.utils import date_range
from keras import layers
from scipy.stats import shapiro
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.feature_selection import VarianceThreshold
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import Normalizer, StandardScaler
from sklearn.svm import LinearSVC, LinearSVR
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
from tensorflow import keras
pd.set_option("display.max_columns", 10)
alt.renderers.enable("jupyterlab")
warnings.filterwarnings("ignore")
# Исходный набор данных
RAW_DATA_PATH = os.path.join("../data/station_data.xlsx")
RANDOM_STATE = 2023
Для возможности создания наиболее качественных в будущем прогнозных моделей, необходимо провести максимально подробный анализ представленных данных:
Загрузка исходного набора данных для исследования
raw_data = pd.read_excel(RAW_DATA_PATH)
raw_data.head(n=3)
| Unnamed: 0 | Температура воздуха [°C] | Unnamed: 2 | Unnamed: 3 | Точка росы [°C] | ... | Unnamed: 19 | Солнечная панель [mV] | АКБ [mV] | АКБ2 [mV] | Unnamed: 23 | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Дата / время | ср.знач | максимум | минимум | ср.знач | ... | минимум | последний | последний | последний | Эталонная эвапотранспирация ET0 [mm] |
| 1 | 2020-08-31 19:00:00 | 19.74 | 19.82 | 19.68 | 16.7 | ... | 18 | 9058 | 6784 | 3643 | NaN |
| 2 | 2020-08-31 18:00:00 | 20.18 | 20.6 | 19.76 | 17.5 | ... | 17.2 | 9298 | 6793 | 3643 | NaN |
3 rows × 24 columns
Заголовок таблицы состоит из двух строк, т.е. составной. Каждый из основных столбцов состоит из нескольких столбцов расширяющих информацию о них.
Дата/время - Описывает дату и время проведения замера
Температура воздуха [°C] - Температура воздуха во время проведения замера со средним, минимальным и максимальным значением, выражена в градусах Цельсия (https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D0%BC%D0%BF%D0%B5%D1%80%D0%B0%D1%82%D1%83%D1%80%D0%B0_%D0%B2%D0%BE%D0%B7%D0%B4%D1%83%D1%85%D0%B0)
Точка росы [°C] - Tемпература воздуха, при которой содержащийся в нём пар достигает состояния насыщения и начинает конденсироваться в росу. Выражена в градусах Цельсия и представлен средним и минимальным значением (https://ru.wikipedia.org/wiki/%D0%A2%D0%BE%D1%87%D0%BA%D0%B0_%D1%80%D0%BE%D1%81%D1%8B)
Солнечная радиация [W/m2] - Электромагнитное и корпускулярное излучение Солнца. Данный параметр не означает радиацию в "бытовом" смыcле от слова ионизирующее излучение. Измеряется мощностью переносимой ею энергии на единицу площади поверхности Ватт/м^2. Представлено средним значением (https://ru.wikipedia.org/wiki/%D0%A1%D0%BE%D0%BB%D0%BD%D0%B5%D1%87%D0%BD%D0%B0%D1%8F_%D1%80%D0%B0%D0%B4%D0%B8%D0%B0%D1%86%D0%B8%D1%8F)
VPD [kPa] - Дефицит давления пара (https://growstuff.ru/blog/poleznaya-informatsiya/rasskazhem-pro-vpd-i-uroven-transpiratsii/), измеряется в килопаскалях (кПа) и выражено средним и минимальным значением
Влажность воздуха [%] - Выражена средним, максимальным и минимальным значениями (https://hvac-school.ru/upload/files/folder_28/tree_inrost.htm). Измеряется в процентах
Осадки [mm] - Количество осадков, измеряется в миллиметарх. (https://www.gismeteo.ru/news/klimat/29223-pochemu-osadki-izmeryayut-v-millimetrah)
Влажность листа [time] - Показания датчика влажности листа. Определяет факт и длительность нахождения листа во влажном состоянии.
Скорость ветра [m/s] - Средняя и максимальная скорость ветра в метрах в секунду.
Влажность почвы [%] - Среднее значение влажности почвы, в процентах
Температура почвы [°C] - Среднее, максимальное и минимальное значение температуры почвы в градусах Цельсия
При певоначальном анализе представленного набора данных было выявлено несколько проблем:
Pandas. Причем в Pandas, первая строка в наборе даннных является названиями подстолбцов, а в данном месте в названии столбца используется автоматическое переименование Unnamed ....Unnamed: 0 и Unnamed: 23 не имеют названий, названия имеют только их подстолбцы. Данное обстоятельство видно есть открыть файл в excel-редакторе (Рисунок 2).Для упрощения работы над такими данными предполагается сделать некоторые преобразования (переименования) чтобы привести их к более распространненному виду. Наиболее приемлемый вариант - использование английских слов (перевод).
Для начала необходимо избавиться от составных столбцов. Для этого сначала заменим первый и последний столбец именами подстолбцов. Далее в цикле пройдем по столбцам и объединим имена с подстолбцамм по правилу - имя столбца_имя подстолбца
original_rows = raw_data.columns
print("Названия оригинальных столбцов: \n\n", original_rows, "\n")
original_subrows = raw_data.iloc[0].values
print("Названия оригинальных подстолбцов: \n\n", original_subrows, "\n")
Названия оригинальных столбцов:
Index(['Unnamed: 0', 'Температура воздуха [°C]', 'Unnamed: 2', 'Unnamed: 3',
'Точка росы [°C]', 'Unnamed: 5', 'Солнечная радиация [W/m2]',
'VPD [kPa]', 'Unnamed: 8', 'Влажность воздуха [%]', 'Unnamed: 10',
'Unnamed: 11', 'Осадки [mm]', 'Влажность листа [минимум]',
'Скорость ветра [m/s]', 'Unnamed: 15', 'Влажность почвы [%]',
'Температура почвы [°C]', 'Unnamed: 18', 'Unnamed: 19',
'Солнечная панель [mV]', 'АКБ [mV]', 'АКБ2 [mV]', 'Unnamed: 23'],
dtype='object')
Названия оригинальных подстолбцов:
['Дата / время' 'ср.знач' 'максимум' 'минимум' 'ср.знач' 'минимум'
'ср.знач' 'ср.знач' 'минимум' 'ср.знач' 'максимум' 'минимум' 'сумма'
'time' 'ср.знач' 'максимум' 'ср.знач' 'ср.знач' 'максимум' 'минимум'
'последний' 'последний' 'последний'
'Эталонная эвапотранспирация ET0 [mm]']
# Измененные имена столбцов
changed_column_names = []
for i, (col, sub_col) in enumerate(zip(original_rows, original_subrows)):
if not str(col).strip().startswith("Unnamed"):
real_col_name = col
if i == 0 or i == (len(original_rows) - 1):
changed_column_names.append(sub_col)
else:
changed_column_names.append(f"{real_col_name}_{sub_col}")
print("Измененнные имена столбцов: \n\n", changed_column_names, "\n")
Измененнные имена столбцов: ['Дата / время', 'Температура воздуха [°C]_ср.знач', 'Температура воздуха [°C]_максимум', 'Температура воздуха [°C]_минимум', 'Точка росы [°C]_ср.знач', 'Точка росы [°C]_минимум', 'Солнечная радиация [W/m2]_ср.знач', 'VPD [kPa]_ср.знач', 'VPD [kPa]_минимум', 'Влажность воздуха [%]_ср.знач', 'Влажность воздуха [%]_максимум', 'Влажность воздуха [%]_минимум', 'Осадки [mm]_сумма', 'Влажность листа [минимум]_time', 'Скорость ветра [m/s]_ср.знач', 'Скорость ветра [m/s]_максимум', 'Влажность почвы [%]_ср.знач', 'Температура почвы [°C]_ср.знач', 'Температура почвы [°C]_максимум', 'Температура почвы [°C]_минимум', 'Солнечная панель [mV]_последний', 'АКБ [mV]_последний', 'АКБ2 [mV]_последний', 'Эталонная эвапотранспирация ET0 [mm]']
Теперь неообходимо заменить исходыне имена в наборе данных, на новые имена, и удалить строку с названиями подстолбцов которая находилась в исходном наборе данных.
# Создание карты исходынх и новых имен столбцов
renamaining_map = {
original_cols: changed_cols
for original_cols, changed_cols in zip(
original_rows, changed_column_names
)
}
# Переименование имен столбцов и копирование в новую переменную
header_changed_data = raw_data.rename(columns=renamaining_map).copy()
# Удаление строки содержащей имена подстолбцов
header_changed_data = header_changed_data.iloc[1:]
header_changed_data.head(3)
| Дата / время | Температура воздуха [°C]_ср.знач | Температура воздуха [°C]_максимум | Температура воздуха [°C]_минимум | Точка росы [°C]_ср.знач | ... | Температура почвы [°C]_минимум | Солнечная панель [mV]_последний | АКБ [mV]_последний | АКБ2 [mV]_последний | Эталонная эвапотранспирация ET0 [mm] | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 2020-08-31 19:00:00 | 19.74 | 19.82 | 19.68 | 16.7 | ... | 18 | 9058 | 6784 | 3643 | NaN |
| 2 | 2020-08-31 18:00:00 | 20.18 | 20.6 | 19.76 | 17.5 | ... | 17.2 | 9298 | 6793 | 3643 | NaN |
| 3 | 2020-08-31 17:00:00 | 20.57 | 21.09 | 20.33 | 17.3 | ... | 17.2 | 8349 | 6787 | 3643 | NaN |
3 rows × 24 columns
На данном этапе нобходимо создать маску для переименования исходных столбцов в новые имена на английском языке, и переименовать столбцы.
translated_columns_map = {
"Дата / время": "datetime",
"Температура воздуха [°C]_ср.знач": "air_temperature_mean",
"Температура воздуха [°C]_максимум": "air_temperature_max",
"Температура воздуха [°C]_минимум": "air_temperature_min",
"Точка росы [°C]_ср.знач": "dew_point_mean",
"Точка росы [°C]_минимум": "dew_point_min",
"Солнечная радиация [W/m2]_ср.знач": "solar_radiation_mean",
"VPD [kPa]_ср.знач": "vpd_mean",
"VPD [kPa]_минимум": "vpd_min",
"Влажность воздуха [%]_ср.знач": "air_humidity_mean",
"Влажность воздуха [%]_максимум": "air_humidity_max",
"Влажность воздуха [%]_минимум": "air_humidity_min",
"Осадки [mm]_сумма": "precipitation",
"Влажность листа [минимум]_time": "leaf_wetness",
"Скорость ветра [m/s]_ср.знач": "wind_speed_mean",
"Скорость ветра [m/s]_максимум": "wind_speed_max",
"Влажность почвы [%]_ср.знач": "soil_wetness_mean",
"Температура почвы [°C]_ср.знач": "soil_temperature_mean",
"Температура почвы [°C]_максимум": "soil_temperature_max",
"Температура почвы [°C]_минимум": "soil_temperature_min",
"Солнечная панель [mV]_последний": "solar_panel",
"АКБ [mV]_последний": "battery1",
"АКБ2 [mV]_последний": "battery2",
"Эталонная эвапотранспирация ET0 [mm]": "eto",
}
translated_header_data = header_changed_data.rename(
columns=translated_columns_map
)
translated_header_data.head(3)
| datetime | air_temperature_mean | air_temperature_max | air_temperature_min | dew_point_mean | ... | soil_temperature_min | solar_panel | battery1 | battery2 | eto | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 2020-08-31 19:00:00 | 19.74 | 19.82 | 19.68 | 16.7 | ... | 18 | 9058 | 6784 | 3643 | NaN |
| 2 | 2020-08-31 18:00:00 | 20.18 | 20.6 | 19.76 | 17.5 | ... | 17.2 | 9298 | 6793 | 3643 | NaN |
| 3 | 2020-08-31 17:00:00 | 20.57 | 21.09 | 20.33 | 17.3 | ... | 17.2 | 8349 | 6787 | 3643 | NaN |
3 rows × 24 columns
excel-файл c таблицей разннообразных изменений24Отсуствующие данные - важный параметр, часто помогает улучшить модель, если правильно обработать отсуствие данных.
(
translated_header_data.isna().sum() / len(translated_header_data) * 100
).apply(math.floor)
datetime 0 air_temperature_mean 0 air_temperature_max 0 air_temperature_min 0 dew_point_mean 0 dew_point_min 0 solar_radiation_mean 0 vpd_mean 0 vpd_min 0 air_humidity_mean 0 air_humidity_max 0 air_humidity_min 0 precipitation 0 leaf_wetness 0 wind_speed_mean 0 wind_speed_max 0 soil_wetness_mean 0 soil_temperature_mean 0 soil_temperature_max 0 soil_temperature_min 0 solar_panel 0 battery1 0 battery2 0 eto 95 dtype: int64
Только столбец eto, который является Эталонной эвапотранспирацией имеет отсутствующие значения, в количестве 95%. Характер отсуствия данных является важной характеристикой, для принятия решения о действиях над данными. В следующем разделе будет исследован характер отсуствия большого количества данных в данном столбце и сделаны соответствущие выводы.
Необходимо посмотреть какие типы данных представлены в наборе
translated_header_data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 3649 entries, 1 to 3649 Data columns (total 24 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 datetime 3649 non-null object 1 air_temperature_mean 3649 non-null object 2 air_temperature_max 3649 non-null object 3 air_temperature_min 3649 non-null object 4 dew_point_mean 3649 non-null object 5 dew_point_min 3649 non-null object 6 solar_radiation_mean 3649 non-null object 7 vpd_mean 3649 non-null object 8 vpd_min 3649 non-null object 9 air_humidity_mean 3649 non-null object 10 air_humidity_max 3649 non-null object 11 air_humidity_min 3649 non-null object 12 precipitation 3649 non-null object 13 leaf_wetness 3649 non-null object 14 wind_speed_mean 3649 non-null object 15 wind_speed_max 3649 non-null object 16 soil_wetness_mean 3649 non-null object 17 soil_temperature_mean 3649 non-null object 18 soil_temperature_max 3649 non-null object 19 soil_temperature_min 3649 non-null object 20 solar_panel 3649 non-null object 21 battery1 3649 non-null object 22 battery2 3649 non-null object 23 eto 152 non-null object dtypes: object(24) memory usage: 684.3+ KB
Все столбцы имеют тип object, что скорее всего свидетельствует о том, что тип данных не удалось определить автоматически, и необходимо вручную идентифицировать типы данных, для корректной работы над дальнейшим анализом.
Визуально оценим тип данных в каждом из столбцов и приведем к правильному типу, исключая столбец eto, так как в нем имеются None - значения.
# Транспонирование таблицы данных, для более удобного анализа
translated_header_data.sample(7, random_state=3).T
| 534 | 1772 | 693 | 2285 | 2870 | 3459 | 2053 | |
|---|---|---|---|---|---|---|---|
| datetime | 2020-08-09 14:00:00 | 2020-06-19 00:00:00 | 2020-08-02 23:00:00 | 2020-05-28 15:00:00 | 2020-05-04 06:00:00 | 2020-04-09 17:00:00 | 2020-06-07 07:00:00 |
| air_temperature_mean | 25.92 | 13.39 | 22.41 | 21.64 | 4.52 | 5.29 | 14.69 |
| air_temperature_max | 26.94 | 14.13 | 23.88 | 22.64 | 4.68 | 5.74 | 15.17 |
| air_temperature_min | 24.74 | 12.9 | 21.57 | 20.29 | 3.97 | 4.81 | 12.79 |
| dew_point_mean | 20.8 | 13.3 | 20.2 | 10.7 | 4.4 | -1.5 | 12.6 |
| dew_point_min | 20 | 12.8 | 19.2 | 9.2 | 3.9 | -1.9 | 12.2 |
| solar_radiation_mean | 360 | 0 | 0 | 860 | 0 | 503 | 5 |
| vpd_mean | 0.87 | 0 | 0.33 | 1.28 | 0 | 0.34 | 0.2 |
| vpd_min | 0.64 | 0 | 0.17 | 1.05 | 0 | 0.32 | 0 |
| air_humidity_mean | 73.79 | 99.85 | 87.86 | 50.32 | 99.97 | 61.44 | 87.89 |
| air_humidity_max | 79.37 | 100 | 93.44 | 56.45 | 99.97 | 63.76 | 99.97 |
| air_humidity_min | 68.64 | 98.59 | 75.34 | 44.45 | 99.97 | 59.84 | 83.39 |
| precipitation | 0 | 0 | 0.2 | 0 | 3.4 | 0 | 0 |
| leaf_wetness | 0 | 0 | 0 | 0 | 60 | 0 | 0 |
| wind_speed_mean | 0.7 | 0.1 | 0.1 | 1.5 | 2.8 | 2 | 0.8 |
| wind_speed_max | 1.2 | 0.5 | 0.7 | 2.3 | 3.6 | 2.7 | 1.8 |
| soil_wetness_mean | 30.09 | 40.85 | 29.57 | 40.01 | 41.75 | 37.87 | 39.15 |
| soil_temperature_mean | 18.4 | 14.2 | 21.4 | 12.1 | 8.5 | 0.1 | 14.4 |
| soil_temperature_max | 19.4 | 17.2 | 23.1 | 12.7 | 9 | 2 | 15.7 |
| soil_temperature_min | 18.1 | 12.6 | 20.6 | 9.7 | 7.8 | -0.5 | 12.1 |
| solar_panel | 9102 | 0 | 0 | 10051 | 0 | 10309 | 6408 |
| battery1 | 6773 | 6502 | 6479 | 6790 | 6411 | 6824 | 6496 |
| battery2 | 3634 | 3663 | 3643 | 3677 | 3696 | 3716 | 3668 |
| eto | NaN | 3.2 | NaN | NaN | NaN | NaN | NaN |
data_types_map = {
"datetime": "datetime64[ns]",
"air_temperature_mean": "float32",
"air_temperature_max": "float32",
"air_temperature_min": "float32",
"dew_point_mean": "float32",
"dew_point_min": "float32",
"solar_radiation_mean": "int32",
"vpd_mean": "float32",
"vpd_min": "float32",
"air_humidity_mean": "float32",
"air_humidity_max": "float32",
"air_humidity_min": "float32",
"precipitation": "float32",
"leaf_wetness": "int32",
"wind_speed_mean": "float32",
"wind_speed_max": "float32",
"soil_wetness_mean": "float32",
"soil_temperature_mean": "float32",
"soil_temperature_max": "float32",
"soil_temperature_min": "float32",
"solar_panel": "int32",
"battery1": "int32",
"battery2": "int32",
}
typed_data = translated_header_data.astype(data_types_map)
typed_data.dtypes
datetime datetime64[ns] air_temperature_mean float32 air_temperature_max float32 air_temperature_min float32 dew_point_mean float32 dew_point_min float32 solar_radiation_mean int32 vpd_mean float32 vpd_min float32 air_humidity_mean float32 air_humidity_max float32 air_humidity_min float32 precipitation float32 leaf_wetness int32 wind_speed_mean float32 wind_speed_max float32 soil_wetness_mean float32 soil_temperature_mean float32 soil_temperature_max float32 soil_temperature_min float32 solar_panel int32 battery1 int32 battery2 int32 eto object dtype: object
Дублирующие друг друга столбцы и строки встечаются в некоторых наборах данных и как минимум приводят к увеличению времени обучения и разработки модели. Необходимо проверить есть ли в наборе данных столбцы и строки с одинаковыми значениями. При их наличии, необходимо оставить только один столбец или строку а остальные дубликаты удалить.
duplicated_cols = []
for i in range(0, len(typed_data.columns)):
col_first = typed_data.columns[i]
for col_second in typed_data.columns[i + 1 :]:
if typed_data[col_first].equals(typed_data[col_second]):
duplicated_cols.append(col_second)
print("Дублирующие столбцы:")
duplicated_cols
Дублирующие столбцы:
[]
В представленном наборе данных дубликатов по столбцам не обнаружено.
print("Дублирующие строки:")
len(typed_data) - len(typed_data.drop_duplicates())
Дублирующие строки:
0
В представленном наборе данных дубликатов по строкам не обнаружено.
Некоторые распределения признаков имеют нулевую или очень низкую дисперсию, и часто могут быть неинформативными, а также нести прооблемы для прогнозирования. Лучше найти их заранее и знать о таких переменных. Необходимо исключить столбец с датой, так как он не сможет быть обработан
sel = VarianceThreshold(threshold=0.0)
sel.fit(typed_data.drop(["datetime"], axis=1))
low_variance_cols = (
len(typed_data.drop(["datetime"], axis=1).columns)
- sel.get_support().sum()
)
print("Количество столбцов с низкой дисперсией:", low_variance_cols)
Количество столбцов с низкой дисперсией: 0
В представленном наборе данных нет переменных которые бы имели только одно постоянное значение.
Проведенное первичное исследование набора данных показывает следующие особенности:
object, что вероятно означает, что pandas не смог автоматически определить их правильно. Таким образом данные были сконверированы вручную для их дальнейшего анализаeto (Эталонная эвапотранспирация) содержит 95% пропусков. Эту особенность данных необходимо исследовать более подробно.На данном этапе предпринимается попытка визуализировать распределение отдельных переменных, взаимодействия переменных и получить больше информации об исходном наборе данных. Полученные результаты могет быть использованы на этапе разработки признаков.
В наборе данных есть паарметры, которые характеризуются тремя значениями: средним, максимальным и минимальным. Будем проводить анализ используя средние заначения переменных, а потом сравним остальные значения переменных со средними, чтобы понять, несут ди они смысловую нагрузку для будущей модели или коррелируют друг с другом.
# Год/или годы в которых происходили измерения
measurements_years = pd.to_datetime(typed_data.datetime).dt.year.unique()
# количество дней с записями
start_measurements_day = pd.to_datetime(typed_data.datetime).dt.date.min()
end_measurements_day = pd.to_datetime(typed_data.datetime).dt.date.max()
measurements_days = (end_measurements_day - start_measurements_day).days
print("Период замеров в годах: ", measurements_years)
print("Период замеров в днях: ", measurements_days)
print("День начала замеров: ", start_measurements_day)
print("День окончания замеров: ", end_measurements_day)
Период замеров в годах: [2020] Период замеров в днях: 152 День начала замеров: 2020-04-01 День окончания замеров: 2020-08-31
Измерения производились только в 2020 году, в течении 152 дней. Начало замеров стартовало 4 апреля, и закончилось 31 августа. Т.е. в месяцы максимально теплого периода года, с середины весты до конца лета.
# Интервал в течении 2020 года
year_period = date_range("2020-01-01", "2020-12-31")
# Дни года в которые производились земеры
year_day_labels = np.zeros(len(year_period))
# Только даты в которые производились замеры
measurement_days = list(pd.to_datetime(typed_data.datetime).dt.date)
# Находим и помечаем дни в которых были замеры
for year_day in range(len(year_period)):
target_day = year_period[year_day]
for measurement_day in measurement_days:
if measurement_day == target_day:
year_day_labels[year_day] = 1
# Визуализируем дни замеров
july.heatmap(
year_period,
year_day_labels,
title="Дни в которых производились замеры",
cmap="github",
month_grid=True,
frame_on=True,
);
Пропущенных дней замеров среди периода эксперимента не наблюдается.
# Несколько записей с начала файла
typed_data.head(3).iloc[:, 0:3]
| datetime | air_temperature_mean | air_temperature_max | |
|---|---|---|---|
| 1 | 2020-08-31 19:00:00 | 19.74 | 19.82 |
| 2 | 2020-08-31 18:00:00 | 20.18 | 20.60 |
| 3 | 2020-08-31 17:00:00 | 20.57 | 21.09 |
# Несколько записей с конца файла
typed_data.tail(3).iloc[:, 0:3]
| datetime | air_temperature_mean | air_temperature_max | |
|---|---|---|---|
| 3647 | 2020-04-01 21:00:00 | 5.70 | 5.92 |
| 3648 | 2020-04-01 20:00:00 | 6.22 | 7.30 |
| 3649 | 2020-04-01 19:00:00 | 8.20 | 8.65 |
Так как период измерений начинается с 04.01.2020 и заканчивается 31.08.31, то видно что в начале файла расположены более поздние замеры, а в конце файла более ранние. Это стоит учитывать при дальнейшей работе с данными.
На этапе предварительного анализа данных, в столбце eto были обнаружены 95% - отсуствующих значений. Посмотрим на них подробнее.
print(
"Количество отсуствующих значений в столбце `eto`:",
len(typed_data[~typed_data["eto"].isna()]),
)
Количество отсуствующих значений в столбце `eto`: 152
# Отображаем столбец времени
typed_data[~typed_data["eto"].isna()]["datetime"].dt.time[:20]
20 00:00:00 44 00:00:00 68 00:00:00 92 00:00:00 116 00:00:00 140 00:00:00 164 00:00:00 188 00:00:00 212 00:00:00 236 00:00:00 260 00:00:00 284 00:00:00 308 00:00:00 332 00:00:00 356 00:00:00 380 00:00:00 404 00:00:00 428 00:00:00 452 00:00:00 476 00:00:00 Name: datetime, dtype: object
Присуствующих значений - 152, столько же, сколько дней производились замеры в наборе данных. Если отфильтровать по столбцу с датаой и посмотреть на часы, где присутствуют данные, то можно увидеть что данные появляются только в 00:00:00 - один раз в сутки. Видимо это значение определяется 1 раз в сутки и добавляется в набор данных в полночь.
Если посмотреть более подробно, то эвапотранспирация – это комбинация двух процессов: испарения воды с поверхности почвы или влажной поверхности растений и испарение воды растением.Этот показатель расчитывается за день и расчитывается по следующей формуле:
где $ETr$ - интенсивность тарансипации, т.е. испарения воды растением (дюймов в день) эталонной культуры (например люцерны), $Kc$ - коэффициент потребления воды культурой, метяющийся в зависимости от фазы развития растения (от 0 до 1), $Ks$ - коэффициент недостатка влаги (обычно от 0 до 1).
Таким образом, понятно что параметр eto, рассчитывается только 1 раз в день, поэтому этот пропуск в данных можно считать намеренным. Таким образом можно заполнить этот столбец значением рассчитанным за прошедшие сутки.
Так как у нас первый замер 04.01.2020 не начинается с полночи, значит мы не можем правильно заполнить отсуствующие в этот день данные, поэтому заполним их средним значением по этому месяцу. Остальные значения можно заполнить установив значение на после расчета по предыдущему дню.
Начинаем циклом проходить с начала файла, т.е. с последенй даты замера, сохранять значения пока встечается NaN, и после обрануженния цифрового значения заполнить предыдущиеNaN найденным значением. Кроме последнего дня, который заполним средним по неделе.
# Проверка на нулевые значения
print(
"Количество значений 0 в переменной `eto`: ",
sum(typed_data["eto"].unique() == 0),
)
Количество значений 0 в переменной `eto`: 0
Так как нулевых значений нет в переменной eto, то можно их использовать для временный значений.
# Создаем копию данных
filled_data = typed_data.copy()
eto_variable_values = typed_data["eto"].values
modified_eto_variable_values = np.zeros(shape=(len(eto_variable_values),))
# Индекс остановки
stop_index = 0
for eto_index, eto_value in enumerate(eto_variable_values):
if not str(eto_value) == "nan":
modified_eto_variable_values[stop_index : eto_index + 1] = eto_value
stop_index = eto_index + 1
# Количество оставшихся пустых значений
null_values_count = sum(modified_eto_variable_values == 0)
# Номер прследенй недели
first_week_number = filled_data.datetime.dt.isocalendar().week.min()
# Отбираем записи относящиеся к последенй неделе, кроме последних пустых
# И возьмем среднее значение по ним
mean_first_week_values = filled_data[
filled_data.datetime.dt.isocalendar().week == first_week_number
][0:-null_values_count]["eto"].mean()
# Заменяем средним значением
modified_eto_variable_values[modified_eto_variable_values == 0] = (
mean_first_week_values
)
# Заменяем значения в столбце `eto` в наборе данных
filled_data["eto"] = modified_eto_variable_values
filled_data[["datetime", "eto"]]
| datetime | eto | |
|---|---|---|
| 1 | 2020-08-31 19:00:00 | 1.80 |
| 2 | 2020-08-31 18:00:00 | 1.80 |
| 3 | 2020-08-31 17:00:00 | 1.80 |
| 4 | 2020-08-31 16:00:00 | 1.80 |
| 5 | 2020-08-31 15:00:00 | 1.80 |
| ... | ... | ... |
| 3645 | 2020-04-01 23:00:00 | 2.15 |
| 3646 | 2020-04-01 22:00:00 | 2.15 |
| 3647 | 2020-04-01 21:00:00 | 2.15 |
| 3648 | 2020-04-01 20:00:00 | 2.15 |
| 3649 | 2020-04-01 19:00:00 | 2.15 |
3649 rows × 2 columns
Данные были заполнены успешно.
Построим распределения переменных, для определения выбросов
analyzed_cols = [
"air_temperature_mean",
"dew_point_mean",
"solar_radiation_mean",
"vpd_mean",
"air_humidity_mean",
"precipitation",
"precipitation",
"leaf_wetness",
"wind_speed_mean",
"soil_wetness_mean",
"soil_temperature_mean",
"solar_panel",
"battery1",
"battery2",
"eto",
]
analyzed_charts = []
for analyzed_col in analyzed_cols:
chart = (
alt.Chart(filled_data)
.mark_point(size=1)
.encode(
x=alt.X("datetime:T", axis=alt.Axis(tickCount=4, grid=True)),
y=analyzed_col,
)
.properties(
width=200,
height=200,
title=["Распределение", "переменной", f"'{analyzed_col}'"],
)
)
analyzed_charts.append(chart)
v1 = alt.vconcat(*(i for i in analyzed_charts[:5]))
v2 = alt.vconcat(*(i for i in analyzed_charts[5:10]))
v3 = alt.vconcat(*(i for i in analyzed_charts[10:]))
(v1 | v2 | v3)
fig = plt.figure(figsize=(10, 20))
ax = fig.gca()
filled_data[analyzed_cols].hist(ax=ax);
На графиках видно, что некоторые переменные содержат выбросы, в том числе и целевая переменная leaf_wetness, имеет одну точку, которая представлена всего одним значением. Считается, что выбросы это данные которые отличаются от основной массы. Если таких данных больше 15%, то они уже не считаются выбросами - The Effects of Outlier Data on Neural Networks Performance
Для того чтобы определить есть ли у переменной выбросы, нуобходимо расчитать квантили распределения а затем межквартильный размах:
Выбросы будут находиться за пределами нижних и верхних границ:
Верхняя граница = $75quantile + (IQR * 1.5)$
Нижняя граница = $25quantile - (IQR * 1.5)$
cols_outliers = {}
for analyzed_col in analyzed_cols:
iqr = filled_data[analyzed_col].quantile(0.75) - filled_data[
analyzed_col
].quantile(0.25)
lower_boundary = filled_data[analyzed_col].quantile(0.25) - (iqr * 1.5)
upper_boundary = filled_data[analyzed_col].quantile(0.75) + (iqr * 1.5)
cols_outliers[analyzed_col] = {
"lower_boundary": lower_boundary,
"upper_boundary": upper_boundary,
"iqr": iqr,
}
pd.DataFrame(cols_outliers).T
| lower_boundary | upper_boundary | iqr | |
|---|---|---|---|
| air_temperature_mean | -3.159999 | 36.279999 | 9.860000 |
| dew_point_mean | -15.849999 | 39.749999 | 13.900000 |
| solar_radiation_mean | -484.500000 | 807.500000 | 323.000000 |
| vpd_mean | -1.095000 | 1.825000 | 0.730000 |
| air_humidity_mean | 17.234993 | 149.435005 | 33.050003 |
| precipitation | 0.000000 | 0.000000 | 0.000000 |
| leaf_wetness | -30.000000 | 50.000000 | 20.000000 |
| wind_speed_mean | -1.950000 | 4.050000 | 1.500000 |
| soil_wetness_mean | 18.025004 | 54.944995 | 9.229998 |
| soil_temperature_mean | -3.650001 | 31.950001 | 8.900001 |
| solar_panel | -14284.500000 | 23807.500000 | 9523.000000 |
| battery1 | 6026.000000 | 7234.000000 | 302.000000 |
| battery2 | 3576.000000 | 3768.000000 | 48.000000 |
| eto | -0.850000 | 6.750000 | 1.900000 |
К этим данным можно вернуться на этапе разработки признаков, для улучшения модели.
Так так мы пытаемся прогнозировать влажность листьев которая имеет тенденции к изменениям в течении дня, кажется, что наиболее правильным вариантом является исследование зависимости в дневных средних распределениях данных. Однако снчала необходимо убедиться что в представленных данных нет странностей (как с целевой переменной). Посмотрим как меняется температура воздуха в месте проведения эксперимента в течении 5 месяцев (с весны по конец лета).
# Подвыборака только с нужными столбцами (дата и параметры с температурами)
temperature_cols = [
"air_temperature_mean",
"dew_point_mean",
"soil_temperature_mean",
]
temp_data_subset = filled_data[["datetime"] + temperature_cols].copy()
# Приведение даты к типу datetime.date для поиска только по дням (исключая часы)
temp_data_subset["datetime"] = temp_data_subset["datetime"].dt.date
# Дата замера
df_data_column = []
# Средняя температура по дням замеров
df_mean_temperature_column = []
# Тип датчика
df_temperature_type_column = []
# Усреднение дневной температуры по средней почасовой по каждому датчику
for temperature_col in temperature_cols:
for uniq_data in temp_data_subset["datetime"].unique():
data_group = temp_data_subset[
temp_data_subset["datetime"] == pd.to_datetime(uniq_data).date()
]
df_data_column.append(str(uniq_data))
df_mean_temperature_column.append(
round(data_group[temperature_col].mean(), 2)
)
df_temperature_type_column.append(temperature_col)
# Сборка DataFrame
mean_sensor_temperature_df = pd.DataFrame(
{
"Дата замера": df_data_column,
"Средняя температура,°C": df_mean_temperature_column,
"Датичк": df_temperature_type_column,
}
)
mean_sensor_temperature_df.head(5)
| Дата замера | Средняя температура,°C | Датичк | |
|---|---|---|---|
| 0 | 2020-08-31 | 17.299999 | air_temperature_mean |
| 1 | 2020-08-30 | 17.809999 | air_temperature_mean |
| 2 | 2020-08-29 | 20.809999 | air_temperature_mean |
| 3 | 2020-08-28 | 27.090000 | air_temperature_mean |
| 4 | 2020-08-27 | 24.760000 | air_temperature_mean |
Для того чтобы лучше увидеть тенденции данных, отобразим его сглаженным
# Сглаживание распределений
def smooth_curve(points, factor=0.8):
smoothed_points = []
for point in points:
if smoothed_points:
previous = smoothed_points[-1]
smoothed_points.append(previous * factor + point * (1 - factor))
else:
smoothed_points.append(point)
return smoothed_points
# Копирование DataFarame в новый
mean_smoothed_sensor_temperature_df = mean_sensor_temperature_df.copy()
mean_smoothed_sensor_temperature_df["Средняя температура,°C"] = smooth_curve(
mean_sensor_temperature_df["Средняя температура,°C"]
)
alt.Chart(mean_smoothed_sensor_temperature_df).mark_line().encode(
x=alt.X("Дата замера:T", axis=alt.Axis(tickCount=4, grid=True)),
y="Средняя температура,°C",
color="Датичк",
).properties(
width=500,
height=200,
title=[
"Тенденция изменения температуры",
"в течении проведения эксперимента",
],
)
На всем протяжении имерений, видно что температуры со всех датчиков изменяются примерно одинаково. Если наблюдается рост температуры одного датчика,то растет и температура другого и наоборот. Так же видно, что колебания температуры снижаются, дипазон изменений становиться меньше к лету, но в конце лета заметно резкое снижение температуры. Можно сделать вывод что данные похожи на реальные и отображают правильное поведение температуры.
Проведем подобный эксперимент с влажностью, в норме она должна иметь похожее поведение
# Подвыборака только с нужными столбцами (дата и параметры с температурами)
wetness_cols = ["air_humidity_mean", "soil_wetness_mean"]
wetness_data_subset = filled_data[["datetime"] + wetness_cols].copy()
# Приведение даты к типу datetime.date для поиска только по дням (исключая часы)
wetness_data_subset["datetime"] = wetness_data_subset["datetime"].dt.date
# Дата замера
df_data_column = []
# Средняя влажность по дням замеров
df_mean_wetness_column = []
# Тип датчика
df_wetness_type_column = []
# Усреднение дневной влажности по средней почасовой по каждому датчику
for wetness_col in wetness_cols:
for uniq_data in wetness_data_subset["datetime"].unique():
data_group = wetness_data_subset[
wetness_data_subset["datetime"]
== pd.to_datetime(uniq_data).date()
]
df_data_column.append(str(uniq_data))
df_mean_wetness_column.append(
round(data_group[wetness_col].mean(), 2)
)
df_wetness_type_column.append(wetness_col)
# Сборка DataFrame
mean_sensor_wetness_df = pd.DataFrame(
{
"Дата замера": df_data_column,
"Средняя влажность,%": df_mean_wetness_column,
"Датичк": df_wetness_type_column,
}
)
mean_sensor_wetness_df.head(5)
| Дата замера | Средняя влажность,% | Датичк | |
|---|---|---|---|
| 0 | 2020-08-31 | 91.160004 | air_humidity_mean |
| 1 | 2020-08-30 | 96.260002 | air_humidity_mean |
| 2 | 2020-08-29 | 99.970001 | air_humidity_mean |
| 3 | 2020-08-28 | 82.330002 | air_humidity_mean |
| 4 | 2020-08-27 | 93.559998 | air_humidity_mean |
# Копирование DataFarame в новый
mean_smoothed_sensor_wetness_df = mean_sensor_wetness_df.copy()
mean_smoothed_sensor_wetness_df["Средняя влажность,%"] = smooth_curve(
mean_smoothed_sensor_wetness_df["Средняя влажность,%"]
)
alt.Chart(mean_smoothed_sensor_wetness_df).mark_line().encode(
x=alt.X("Дата замера:T", axis=alt.Axis(tickCount=4, grid=True)),
y="Средняя влажность,%",
color="Датичк",
).properties(
width=500,
height=200,
title=[
"Тенденция изменения влажности воздуха",
"в течении проведения эксперимента",
],
)
Как и ожидалось, влажность имеет похожее распределение. В целом влажность земли и воздуха также сильно отличаются но следуют похожей тенденции как показатели температуры, т.е. увеличение влажности одного датчика тоже подразумевает увеличения влажности другого, только в разных диапазаонах.
Целевая переменная, представлена целочисленными значениями. Посмотрим на количество уникальных значений в переменной.
filled_data.leaf_wetness.unique()
array([ 0, 5, 60, 45, 10, 50, 30, 35, 25, 20, 40, 55, 15, 75, 70],
dtype=int32)
Переменная содержит всего 15 различных значений, причем все кратные 5. Посмотрим визуально как распределены значения.
value_counts_leaf_wetness_df = (
filled_data.leaf_wetness.value_counts().reset_index()
)
value_counts_leaf_wetness_df.columns = ["Значение", "Количество"]
alt.Chart(value_counts_leaf_wetness_df).mark_bar().encode(
x="Значение",
y="Количество",
).properties(
title=[
"Распределение целевой переменной leaf_wetness",
"по количеству дискретных значений",
]
)
Распределение значений очень неравномерно. Большое количество нулевых значение и очень маленькое количество других.
filled_data.leaf_wetness.value_counts()
0 2669 60 690 45 82 5 28 30 25 75 23 40 22 10 20 15 17 35 15 25 15 20 15 50 14 55 13 70 1 Name: leaf_wetness, dtype: int64
Название переменной в исходном наборе данных выглядело как Влажность листа [минимум]', 'time. Изначально можно было предположить, что time означает время, в течении которого лист был влажным. Так как данные представлены с переодичностью в 1 час. Изучив погодную станцию содержащую похожий функционал (https://github.com/polyakovyevgeniy/portfolio_ai/blob/master/Meteobot-Rukovodstvo-RU_ver.1.2.pdf), на странице 15, было указано, что станция собирает даннные каждые 10 минут и отправляет один раз в час. Т.е. ситуация походжа на представленную в данном наборе данных. Т.е. если например взять одно из значений котрое = 15, то получается что 15 минут за этот час лист был влажным. Однако в данных находятся значения больше чем 60. Таким образом данное предположение не соответствует действительности.
Видеофайл размезщенный на Youtube, рассказывает о принципе рабты похожего датчика, где упоминается о том, что он способен измерять уровень влажности. Представленные значения выше 60 могут подходить под уровень влажности в %. Однако остается загадкой, почему этот уровень имеет целочисленные значения, хотя в других переменных уровень влажности представлен как непрерывное значение.
YouTubeVideo("caz1Mspxn-M", width=600)
Исходя из этого можно сделать вывод о странности представленной целевой переменной. Названия в заголовке переменной в таблице данных противоречит с ее реальниыми значениями. В данном случае допкстим, что представленные значения - это уровень влажности и произведем попытку решить данную задачу.
В случае сложности предсказания представленной целевой переменной попробуем решить задачу как классификацию, где любое значение отличное от нуля будет соответсвовать наличию любой влажности на листе а 0 отсутсвию. К примеру если преодически предсказывать по имеющимся данным отсуствие и наличие влаги, можно делать выводы о предолжительности нахождения влаги на листе, что тоже может оказаться полезным.
# Подвыборка только с datetime и целевой переменной
leaf_wetness_subset = filled_data[["datetime", "leaf_wetness"]].copy()
leaf_wetness_subset["datetime"] = leaf_wetness_subset["datetime"].dt.date
df_data_column = []
df_mean_leaf_wetness_column = []
# Усреднение дневной температуры по средней почасовой по каждому датчику
for uniq_data in leaf_wetness_subset["datetime"].unique():
data_group = leaf_wetness_subset[
leaf_wetness_subset["datetime"] == pd.to_datetime(uniq_data).date()
]
df_data_column.append(str(uniq_data))
df_mean_leaf_wetness_column.append(
round(data_group["leaf_wetness"].mean(), 2)
)
# Сборка DataFrame
mean_sensor_leaf_wetness_df = pd.DataFrame(
{
"Дата замера": df_data_column,
"Средняя влажность,%": df_mean_leaf_wetness_column,
}
)
mean_sensor_leaf_wetness_df.head(5)
| Дата замера | Средняя влажность,% | |
|---|---|---|
| 0 | 2020-08-31 | 39.25 |
| 1 | 2020-08-30 | 48.12 |
| 2 | 2020-08-29 | 47.08 |
| 3 | 2020-08-28 | 1.25 |
| 4 | 2020-08-27 | 26.25 |
# Копирование DataFarame в новый
mean_smothed_sensor_leaf_wetness_df = mean_sensor_leaf_wetness_df.copy()
mean_smothed_sensor_leaf_wetness_df["Средняя влажность,%"] = smooth_curve(
mean_smothed_sensor_leaf_wetness_df["Средняя влажность,%"]
)
alt.Chart(mean_smothed_sensor_leaf_wetness_df).mark_line().encode(
x="Дата замера:T",
y="Средняя влажность,%",
).properties(
title=[
"Распределение сглаженной целевой переменной leaf_wetness",
"по периоду измерений",
],
width=700,
)
В целом видно что для влажности листьев свойственна повторяющаяся цикличность, налюбюдаются переодические всплески влжности и падения. Есть более выраженные всплески влажности, они наблюдаются в началае лета и ближе к его концу, но их к сожалению мало, и скорее всего нужно будет отбросить как выбросы. Посмотрим более подробно на цикличность среднего месяца, средней недели и среднего дня.
среднего месяца, средней недели и среднего дня.¶# Подвыборка только с datetime и целевой переменной
leaf_wetness_subset = filled_data[["datetime", "leaf_wetness"]].copy()
# Помечаем день месяца, день недели, и час
leaf_wetness_subset["day"] = leaf_wetness_subset.datetime.dt.day
leaf_wetness_subset["dayofweek"] = leaf_wetness_subset.datetime.dt.dayofweek
leaf_wetness_subset["hour"] = leaf_wetness_subset.datetime.dt.hour
# Максимальное и минимальное количество дней в месяце за весь период измерений
max_month_length = leaf_wetness_subset.datetime.dt.day.max()
min_month_length = leaf_wetness_subset.datetime.dt.day.min()
# Максимальное и минимальное количество дней в неделе за весь период измерений
max_week_length = leaf_wetness_subset.datetime.dt.dayofweek.max()
min_week_length = leaf_wetness_subset.datetime.dt.dayofweek.min()
# Максимальное и минимальное количество часов в дне за весь период измерений
max_hour_length = leaf_wetness_subset.datetime.dt.hour.max()
min_hour_length = leaf_wetness_subset.datetime.dt.hour.min()
# DataFrame по каждому из периодов
mean_leaf_wetness_key_periods_dfs = []
# Усреднение дневной температуры по средней почасовой по каждому датчику
for period, max_value, min_value, title in zip(
["day", "dayofweek", "hour"],
[max_month_length, max_week_length, max_hour_length],
[min_month_length, min_week_length, min_hour_length],
["День месяца", "День недели", "Час дня"],
):
df_data_column = []
df_mean_wetness_column = []
for period_value in range(min_value, max_value + 1):
mean_leaf_wetness = round(
leaf_wetness_subset[leaf_wetness_subset[period] == period_value][
"leaf_wetness"
].mean(),
2,
)
df_data_column.append(period_value)
df_mean_wetness_column.append(mean_leaf_wetness)
mean_leaf_wetness_key_periods_dfs.append(
pd.DataFrame(
{
title: df_data_column,
"Средняя влажность,%": df_mean_wetness_column,
}
)
)
for key_period_df in mean_leaf_wetness_key_periods_dfs:
display(key_period_df.sample(2))
print("\n")
| День месяца | Средняя влажность,% | |
|---|---|---|
| 30 | 31 | 16.18 |
| 29 | 30 | 13.54 |
| День недели | Средняя влажность,% | |
|---|---|---|
| 4 | 4 | 11.05 |
| 2 | 2 | 18.99 |
| Час дня | Средняя влажность,% | |
|---|---|---|
| 15 | 15 | 10.53 |
| 5 | 5 | 22.47 |
# Графики средних по казателей влажности листьев
# по периодам ['День месяца', 'День недели', 'Час дня']
mean_leaf_wetness_key_period_charts = []
mean_leaf_column_name = "Средняя влажность,%"
for mean_leaf_wetness_key_period_df, period_value_col_name in zip(
mean_leaf_wetness_key_periods_dfs,
[df.columns[0] for df in mean_leaf_wetness_key_periods_dfs],
):
# Копирование DataFarame в новый
mean_smoothed_leaf_wetness_key_period_df = (
mean_leaf_wetness_key_period_df.copy()
)
mean_smoothed_leaf_wetness_key_period_df[mean_leaf_column_name] = (
smooth_curve(
mean_smoothed_leaf_wetness_key_period_df[mean_leaf_column_name]
)
)
chart_base = alt.Chart(mean_leaf_wetness_key_period_df).encode(
x=period_value_col_name,
y=mean_leaf_column_name,
)
chart_bar = chart_base.mark_bar().encode(
x=period_value_col_name,
y=mean_leaf_column_name,
)
chart_line = (
alt.Chart(mean_smoothed_leaf_wetness_key_period_df)
.mark_line(color="red")
.encode(
x=period_value_col_name,
y=mean_leaf_column_name,
)
)
compound_chart = (chart_bar + chart_line).properties(
title=[
"Распределение целевой переменной leaf_wetness",
f"по периоду '{period_value_col_name}'",
],
width=700,
height=100,
)
mean_leaf_wetness_key_period_charts.append(compound_chart)
alt.vconcat(*mean_leaf_wetness_key_period_charts)
В течении месяца всплесков происходит большое количество, если смотреть в недельном выражении то в среднем во вторник влажность выше чем в другие дни. Если рассматривать тенденции в дневном выражении то всреднем с 3 часов ночи до 8 утра наблюдается сама высокая влажность листьев. Эти все особенности можно будет использовать на этапе разработки признаков, если качество модели будет не достаточным для ее использования в производственной среде.
Рассмотрим распределение средних показателей переменных wind_speed_mean, precipitation, solar_radiation_mean, vpd_mean
# Подвыборака только с нужными столбцами (дата и параметры с температурами)
other_cols = [
"wind_speed_mean",
"precipitation",
"solar_radiation_mean",
"vpd_mean",
]
other_data_subset = filled_data[["datetime"] + other_cols].copy()
# Приведение даты к типу datetime.date для поиска только по дням (исключая часы)
other_data_subset["datetime"] = other_data_subset["datetime"].dt.date
mean_other_dfs = []
mean_other_titles = [
"Скорость ветра,m/s",
"Осадки,mm",
"Солнечная радиация,W/m2",
"VPD,kPa",
]
# Усреднение дневных показателей по каждому датчику
for other_col, title in zip(
other_cols,
mean_other_titles,
):
# Дата замера
df_data_column = []
# Средняя влажность по дням замеров
df_mean_other_column = []
for uniq_data in other_data_subset["datetime"].unique():
data_group = other_data_subset[
other_data_subset["datetime"] == pd.to_datetime(uniq_data).date()
]
df_data_column.append(str(uniq_data))
df_mean_other_column.append(round(data_group[other_col].mean(), 2))
mean_other_dfs.append(
pd.DataFrame(
{"Дата замера": df_data_column, title: df_mean_other_column}
)
)
for mean_other_df in mean_other_dfs:
display(mean_other_df.sample(2))
print("\n")
| Дата замера | Скорость ветра,m/s | |
|---|---|---|
| 15 | 2020-08-16 | 0.53 |
| 40 | 2020-07-22 | 0.38 |
| Дата замера | Осадки,mm | |
|---|---|---|
| 29 | 2020-08-02 | 0.2 |
| 18 | 2020-08-13 | 0.0 |
| Дата замера | Солнечная радиация,W/m2 | |
|---|---|---|
| 108 | 2020-05-15 | 220.88 |
| 148 | 2020-04-05 | 85.00 |
| Дата замера | VPD,kPa | |
|---|---|---|
| 152 | 2020-04-01 | 0.52 |
| 130 | 2020-04-23 | 0.19 |
mean_other_charts = []
for mean_other_df, title in zip(mean_other_dfs, mean_other_titles):
# Копирование DataFarame в новый
mean_smooth_other_df = mean_other_df.copy()
mean_smooth_other_df[title] = smooth_curve(mean_smooth_other_df[title])
chart = (
alt.Chart(mean_smooth_other_df)
.mark_line()
.encode(
x="Дата замера:T",
y=title,
)
.properties(
title=[
f"Распределение сглаженной переменной '{title}'",
"по периоду измерений",
],
width=700,
height=200,
)
)
mean_other_charts.append(chart)
alt.vconcat(*mean_other_charts)
Показатели Скорость ветра, Осадки, Солнечная радиация, VPD тоже имеют тенденции к переодическим изменениям, можно изучить их более подробно во взаимодействии с целевой переменной
Сравним все исследованные показатели вместе по измененям в течении дня
cols_for_analysis = [
"leaf_wetness",
"air_temperature_mean",
"soil_temperature_mean",
"dew_point_mean",
"air_humidity_mean",
"soil_wetness_mean",
"vpd_mean",
"precipitation",
"wind_speed_mean",
"solar_radiation_mean",
]
cols_titles = [
"Влажность листа,%",
"Температура воздуха,°C",
"Температура земли,°C",
"Точка росы,°C",
"Влажность воздуха,%",
"Влажность земли,%",
"VPD,kPa",
"Осадки,mm",
"Скорость ветра,m/s",
"Солдечная радиация,W/m2",
]
charts_arr = []
data_subset = filled_data[["datetime"] + cols_for_analysis].copy()
# Приведение даты к типу datetime
data_subset["datetime"] = pd.to_datetime(data_subset.datetime.copy()).copy()
data_subset["dayofyear"] = data_subset["datetime"].dt.dayofyear.copy()
data_subset["hour"] = data_subset["datetime"].dt.hour
for col in cols_for_analysis:
data_subset[col] = smooth_curve(data_subset[col].values)
select_dayofyear = alt.selection_single(
name="select",
fields=["dayofyear"],
init={"dayofyear": 145},
bind=alt.binding_range(min=92, max=244, step=1),
)
for i, col in enumerate(cols_for_analysis):
width = 310
height = 100
color = "#3287a8"
if col == "leaf_wetness":
# width = 680
# height = 100
color = "#a83240"
chart = (
alt.Chart(data_subset)
.mark_line()
.encode(
x=alt.X("hour:N", axis=alt.Axis(grid=True), title="Час суток"),
y=alt.Y(col, title=cols_titles[i].split(" ")),
color=alt.value(color),
strokeWidth=alt.value(3),
)
.properties(width=width, height=height)
)
charts_arr.append(chart)
v1 = alt.vconcat(*(i for i in charts_arr[:5]))
v2 = alt.vconcat(*(i for i in charts_arr[5:]))
(v1 | v2).add_selection(select_dayofyear).transform_filter(select_dayofyear)
Сравнение всех показателей по среднему дню
cols_for_analysis = [
"leaf_wetness",
"air_temperature_mean",
"soil_temperature_mean",
"dew_point_mean",
"air_humidity_mean",
"soil_wetness_mean",
"vpd_mean",
"precipitation",
"wind_speed_mean",
"solar_radiation_mean",
]
cols_titles = [
"Влажность листа,%",
"Температура воздуха,°C",
"Температура земли,°C",
"Точка росы,°C",
"Влажность воздуха,%",
"Влажность земли,%",
"VPD,kPa",
"Осадки,mm",
"Скорость ветра,m/s",
"Солдечная радиация,W/m2",
]
charts_arr = []
data_subset = filled_data[["datetime"] + cols_for_analysis].copy()
# Приведение даты к типу datetime
data_subset["datetime"] = pd.to_datetime(data_subset.datetime.copy()).copy()
data_subset["hour"] = data_subset["datetime"].dt.hour
max_hour = data_subset["hour"].max()
min_hour = data_subset["hour"].min()
mean_hour_dfs = []
# Усреднение дневных показателей по каждому датчику
for col_for_analysis, title in zip(
cols_for_analysis,
cols_titles,
):
# Дата замера
df_data_column = []
# Средняя влажность по дням замеров
df_mean_hour_column = []
for hour in range(min_hour, max_hour + 1):
data_group = data_subset[data_subset["hour"] == hour]
df_data_column.append(str(hour))
df_mean_hour_column.append(
round(data_group[col_for_analysis].mean(), 2)
)
mean_hour_dfs.append(
pd.DataFrame(
{"Час суток": df_data_column, title: df_mean_hour_column}
)
)
for i, col in enumerate(cols_titles):
color = "#a83240"
if col == "Влажность листа,%":
color = "green"
chart = (
alt.Chart(mean_hour_dfs[i])
.mark_area()
.encode(
x=alt.X("Час суток:Q", axis=alt.Axis(grid=True)),
y=alt.Y(col, title=cols_titles[i].split(" ")),
color=alt.value(color),
strokeWidth=alt.value(2),
)
.properties(width=310, height=150)
)
charts_arr.append(chart)
v1 = alt.vconcat(*(i for i in charts_arr[:5]))
v2 = alt.vconcat(*(i for i in charts_arr[5:]))
(v1 | v2)
Усреднив все часовые показатели по всем наблюдениям можно увидеть как некоторые показатели имеют тенденцию к повторению значений других переменных. Например заметно некоторые похожие тенденции во влажности воздуха и влажности листьев и обратную тенденцию между влажностью листьев и VPD. Температура воздуха и точка росы имеют похожее поведение. Увеличение солнечной радиации, скорости ветра и повышение температуры воздуха приводит к уменьшению влажности листьев.
Некоторые переменные имеют дополнительные данные (кроме среднего) в виде минимальных и максимальных значений. Скорее всего эти данные будут повторять средние значения, однако необходимо убедиться, что это так.
# Подвыборака только с нужными столбцами (дата и параметры с температурами)
mean_max_min_cols = [
["air_temperature_mean", "air_temperature_max", "air_temperature_min"],
["dew_point_mean", "dew_point_min"],
["vpd_mean", "vpd_min"],
["air_humidity_mean", "air_humidity_max", "air_humidity_min"],
["wind_speed_mean", "wind_speed_max"],
["soil_temperature_mean", "soil_temperature_max", "soil_temperature_min"],
]
mean_max_min_titles = [
"Температура воздуха,°C",
"Точка росы,°C",
"VPD,kPa",
"Влажность воздуха,%",
"Скорость ветра,m/s",
"Температура почвы,°C",
]
mean_max_min_subset = filled_data[
["datetime"] + [y for x in mean_max_min_cols for y in x]
].copy()
# Приведение даты к типу datetime.date для поиска только по дням (исключая часы)
mean_max_min_subset["datetime"] = mean_max_min_subset["datetime"].dt.date
# Усреднение дневной влажности по средней почасовой по каждому датчику
mean_max_min_sensor_arr = []
for sensor_subset, title in zip(mean_max_min_cols, mean_max_min_titles):
# Дата замера
df_data_column = []
# Средняя влажность по дням замеров
df_mean_max_min_column = []
# Тип датчика
df_mean_max_min_type_column = []
for sensor in sensor_subset:
for uniq_data in mean_max_min_subset["datetime"].unique():
data_group = mean_max_min_subset[
mean_max_min_subset["datetime"]
== pd.to_datetime(uniq_data).date()
]
df_data_column.append(str(uniq_data))
df_mean_max_min_column.append(round(data_group[sensor].mean(), 2))
df_mean_max_min_type_column.append(sensor)
mean_max_min_sensor_arr.append(
pd.DataFrame(
{
"Дата замера": df_data_column,
title: df_mean_max_min_column,
"Датчик": df_mean_max_min_type_column,
}
)
)
mean_max_min_sensor_chart_arr = []
for mean_max_min_sensor_df in mean_max_min_sensor_arr:
mean_max_min_smooth_sensor_df = mean_max_min_sensor_df.copy()
title = mean_max_min_smooth_sensor_df.columns[1]
mean_max_min_smooth_sensor_df[title] = smooth_curve(
mean_max_min_smooth_sensor_df[title]
)
chart = (
alt.Chart(mean_max_min_smooth_sensor_df)
.mark_line()
.encode(x="Дата замера:T", y=title, color="Датчик")
.properties(width=300, height=150)
)
mean_max_min_sensor_chart_arr.append(chart)
v1 = alt.vconcat(*(i for i in mean_max_min_sensor_chart_arr[:3]))
v2 = alt.vconcat(*(i for i in mean_max_min_sensor_chart_arr[3:]))
(v1 | v2)
Как и ожидалось максимальные и минимальные значения аналогичны средним, поэтому врядли могут принести большую пользу для улучшению модели.
# Подвыборака только с нужными столбцами (дата и параметры с температурами)
battery_cols = ["solar_panel", "battery1", "battery2"]
# Копируем выборку
battery_data_subset = filled_data[["datetime"] + battery_cols].copy()
# Приведение даты к типу datetime.date для поиска только по дням (исключая часы)
battery_data_subset["datetime"] = battery_data_subset["datetime"].dt.date
# Дата замера
df_data_column = []
# Средняя влажность по дням замеров
df_mean_battery_charge_column = []
# Тип батареи
df_battery_charge_type_column = []
# Усреднение зарада батареи по дням по каждому датчику
for battery_col in battery_cols:
for uniq_data in battery_data_subset["datetime"].unique():
data_group = battery_data_subset[
battery_data_subset["datetime"]
== pd.to_datetime(uniq_data).date()
]
df_data_column.append(str(uniq_data))
df_mean_battery_charge_column.append(
int(data_group[battery_col].mean())
)
df_battery_charge_type_column.append(battery_col)
# Сборка DataFrame
mean_battery_charge_df = pd.DataFrame(
{
"Дата замера": df_data_column,
"Средний заряд,mV": df_mean_battery_charge_column,
"Датичк": df_battery_charge_type_column,
}
)
mean_battery_charge_df.head(5)
| Дата замера | Средний заряд,mV | Датичк | |
|---|---|---|---|
| 0 | 2020-08-31 | 5264 | solar_panel |
| 1 | 2020-08-30 | 3909 | solar_panel |
| 2 | 2020-08-29 | 3331 | solar_panel |
| 3 | 2020-08-28 | 4473 | solar_panel |
| 4 | 2020-08-27 | 4324 | solar_panel |
# Копирование DataFarame в новый
mean_smoothed_battery_charge_df = mean_battery_charge_df.copy()
alt.Chart(mean_battery_charge_df).mark_bar().encode(
x=alt.X("Дата замера:T", axis=alt.Axis(tickCount=4, grid=True)),
y="Средний заряд,mV",
color="Датичк",
).properties(
width=700,
height=200,
title=[
"Заряд батарей и тока выдаваемого солнчной панелью",
"в течении проведения эксперимента",
],
)
Заряд батарей всегда находиться в стабильном состоянии. Напряжение выдаваемое солнечной панелью меняется, так как это зависит от солнечного света. На графике преставлены средние значения заряда. Чтобы убедиться выдает ли солнечная батарея нулевое напряжение в ночное время, можно посмотреть исходные данные.
filled_data[filled_data.solar_panel == 0][["datetime", "solar_panel"]][:5]
| datetime | solar_panel | |
|---|---|---|
| 14 | 2020-08-31 06:00:00 | 0 |
| 15 | 2020-08-31 05:00:00 | 0 |
| 16 | 2020-08-31 04:00:00 | 0 |
| 17 | 2020-08-31 03:00:00 | 0 |
| 18 | 2020-08-31 02:00:00 | 0 |
filled_data[filled_data.solar_panel != 0][["datetime", "solar_panel"]][:5]
| datetime | solar_panel | |
|---|---|---|
| 1 | 2020-08-31 19:00:00 | 9058 |
| 2 | 2020-08-31 18:00:00 | 9298 |
| 3 | 2020-08-31 17:00:00 | 8349 |
| 4 | 2020-08-31 16:00:00 | 9125 |
| 5 | 2020-08-31 15:00:00 | 9605 |
Видно, что в дневное время выдаваемое напряжение не нулевое, тогда как ночью оно равно нулю, что говорит о том, что скорее всего замеры были сделаны на улице (не в теплице).
В целом, значения заряда батарей стабильны и нормальны, и врядли могут как-то влиять на процесс образования влаги на поверхности листа, скорее всего их нужно удалить за ненадобностью.
deleted_battery_data = filled_data.drop(
["solar_panel", "battery1", "battery2"], axis=1
)
print("Количество столбцов с элементами питания: ", len(filled_data.columns))
print(
"Количество столбцов без элементов питания: ",
len(deleted_battery_data.columns),
)
Количество столбцов с элементами питания: 24 Количество столбцов без элементов питания: 21
... для проверки на нормальность для линейных моделей...
Посмотрим корреляции переменных между собой
corr = (
deleted_battery_data[
[
"leaf_wetness",
"air_temperature_mean",
"soil_temperature_mean",
"dew_point_mean",
"air_humidity_mean",
"soil_wetness_mean",
"vpd_mean",
"precipitation",
"wind_speed_mean",
"solar_radiation_mean",
]
]
).corr(numeric_only=True, method="spearman")
mask = np.zeros_like(corr, dtype=np.bool_)
mask[np.triu_indices_from(mask)] = (
True # Верхнему треугольнику присваиваем True
)
plt.figure(figsize=(5, 5))
sns.set(font_scale=0.8)
sns.heatmap(
corr,
mask=mask,
vmax=1,
center=0,
annot=True,
fmt=".1f",
square=True,
linewidths=0.5,
cbar_kws={"shrink": 1},
);
Определенную корреляцию с целевой переменной имеет vpd и влажность воздуха. Это говорит о том, что скорее всего линейные модели не принесут высокого качества моделирования. На этапе базового решения попробуем построить линейную модель, чтобы убедиться в этом предположении.
...
Ellipsis
Исходя из проведенного исследования, можно сделать вывод, что целевая переменная не имеет линейной зависимости от параметров, поэтому использование линейной регресси врядли принесет хорошие результаты. Так как цель данной работы определить возможность получения прогнозной модели по имеющимся данным, и учитывая сложившуюся ситуацию будем исползовать разнородные модели чтобы оценить какой тип наиболее приемлем в данной задаче и в данных условиях. Будем обучать используя: метод опопрных векторов, деревья решений, случайный лес, градиентный бустинг деревьев решений и нейронные сети.
Для данной задачи вполне подойдет стандартный коэфициент детерминации $R^2$, однако можно использовать дополнительно среднюю абсолютную (mse) ошибку чтобы понять на сколько сильно в обсалютных числах наша модель будет ошибаться.
Набор даннных содердит 3649 объектов, что не является большим числом но и не сильно мало. Попробуем сначала обучить модель без использования кросс-валидации, и будем надеяться, что данных хватит для обобщения модели. Если модель будет недообучаться тогда используем кросс-валидацию вместо тестового набора данных для экономии ценных объектов. Но в случае с нейронными сетями все же нужно будет использовать проверочный набор данных, иначе сложно будет найти баланс между недообученной и переобученной моделью.
# Удаление колонки "datetime"
modeling_data = deleted_battery_data.drop(["datetime"], axis=1).copy()
# Разделение на тренировочный и тестовый набор данных
X_reg_train, X_reg_test, y_reg_train, y_reg_test = train_test_split(
modeling_data.drop(["leaf_wetness"], axis=1),
modeling_data["leaf_wetness"],
random_state=RANDOM_STATE,
shuffle=True,
test_size=0.2,
)
print("Размеры тренировочной выборки:")
display(X_reg_train.shape)
print("Размеры тестовой выборки:")
display(X_reg_test.shape)
Размеры тренировочной выборки:
(2919, 19)
Размеры тестовой выборки:
(730, 19)
modeling_data_classification = modeling_data.copy()
modeling_data_classification.loc[
modeling_data_classification["leaf_wetness"] > 1, "leaf_wetness"
] = 1
# Разделение на тренировочный и тестовый набор данных
X_cls_train, X_cls_test, y_cls_train, y_cls_test = train_test_split(
modeling_data_classification.drop(["leaf_wetness"], axis=1),
modeling_data_classification["leaf_wetness"],
random_state=RANDOM_STATE,
shuffle=True,
test_size=0.2,
)
print("Количество классов:")
display(modeling_data_classification["leaf_wetness"].value_counts())
print("Размеры тренировочной выборки:")
display(X_cls_train.shape)
print("Размеры тестовой выборки:")
display(X_test.shape)
Количество классов:
0 2669 1 980 Name: leaf_wetness, dtype: int64
Размеры тренировочной выборки:
(2919, 19)
Размеры тестовой выборки:
(913, 19)
Обучим без предобработок и настроек классические ML-модели в качестве базового решения.
Обучим линейную регрессию. Для линейной регрессии нужно стандартизировать или нормализовать данные. Что лучше продходит для данных представленных распределений данных, можно узнать с помощью тестирования. На следующем этапе можно будет определиться с этим.
baseline_reg_scaler = StandardScaler()
X_reg_train_scaled = baseline_reg_scaler.fit_transform(X_reg_train)
X_reg_test_scaled = baseline_reg_scaler.transform(X_reg_test)
baseline_linreg_reg_model = LinearRegression().fit(
X_reg_train_scaled, y_reg_train
)
display(
baseline_linreg_reg_model.score(X_reg_train_scaled, y_reg_train.values)
)
display(baseline_linreg_reg_model.score(X_reg_test_scaled, y_reg_test.values))
0.3999891497607484
0.3835676710677669
Сразу видно плохое качество линейной модели, так как действительно исходя из матрицы корреляций было понятно, что только две переменные имели небольшую корреляцию с целевой перменной.
Для моделей на основе деревьев не нужно применять масштабирование данных
baseline_dtree_reg_model = DecisionTreeRegressor()
baseline_dtree_reg_model.fit(X_reg_train, y_reg_train)
display(baseline_dtree_reg_model.score(X_reg_train, y_reg_train))
display(baseline_dtree_reg_model.score(X_reg_test, y_reg_test))
1.0
0.5185649046709446
Деревья решений сразу преобучаются, поэтому обязательно для них нужно производить настройку гиперпараметров
baseline_rf_reg_model = RandomForestRegressor()
baseline_rf_reg_model.fit(X_reg_train, y_reg_train)
display(baseline_rf_reg_model.score(X_reg_train, y_reg_train))
display(baseline_rf_reg_model.score(X_reg_test, y_reg_test))
0.9536238152288552
0.729635378925314
Случайный лес тоже сильно переобучился, но уже показывает более качественные результаты
baseline_xgb_reg_model = xgboost.XGBRegressor()
baseline_xgb_reg_model.fit(X_reg_train, y_reg_train)
display(baseline_xgb_reg_model.score(X_reg_train, y_reg_train))
display(baseline_xgb_reg_model.score(X_reg_test, y_reg_test))
0.9923912318107077
0.758851654614346
Среди выбранных моделей, градиентный бустинг сразу показывает наиболее высокие результаты.
baseline_cls_scaler = StandardScaler()
X_cls_train_scaled = baseline_cls_scaler.fit_transform(X_cls_train)
X_cls_test_scaled = baseline_cls_scaler.transform(X_cls_test)
baseline_logreg_clc_model = LogisticRegression().fit(
X_cls_train_scaled, y_cls_train
)
display(
roc_auc_score(
baseline_logreg_clc_model.predict(X_cls_train_scaled),
y_cls_train.values,
)
)
display(
roc_auc_score(
baseline_logreg_clc_model.predict(X_cls_test_scaled),
y_cls_test.values,
)
)
0.8211316721767364
0.8211992588123761
baseline_dtree_cls_model = DecisionTreeClassifier().fit(
X_cls_train_scaled, y_cls_train
)
display(
roc_auc_score(
baseline_dtree_cls_model.predict(X_cls_train_scaled),
y_cls_train.values,
)
)
display(
roc_auc_score(
baseline_dtree_cls_model.predict(X_cls_test_scaled),
y_cls_test.values,
)
)
1.0
0.8431286549707603
baseline_rf_cls_model = RandomForestClassifier().fit(
X_cls_train_scaled, y_cls_train
)
display(
roc_auc_score(
baseline_rf_cls_model.predict(X_cls_train_scaled),
y_cls_train.values,
)
)
display(
roc_auc_score(
baseline_rf_cls_model.predict(X_cls_test_scaled),
y_cls_test.values,
)
)
1.0
0.9022908622908623
baseline_xgb_cls_model = xgboost.XGBClassifier().fit(
X_cls_train_scaled, y_cls_train
)
display(
roc_auc_score(
baseline_xgb_cls_model.predict(X_cls_train_scaled),
y_cls_train.values,
)
)
display(
roc_auc_score(
baseline_xgb_cls_model.predict(X_cls_test_scaled),
y_cls_test.values,
)
)
1.0
0.8956143494186973
Базовое решение показало, что наиболее приемлемыми вариантами для моделирования являеются модели случайного леса и градиентного бустинга деревьев решений. Эти модели и будут дальше использоваться как основные.
Нейронные сети могут оказаться как приемлемым вариантом так и не приемлемым, данных может оказаться слишком мало и нейронная сеть будет или переобучаться или вовсе недообучаться в зависимости от обобщающей способности данных.
Для нейронных сетей определить приемлемый вариант масштабирования данных. Стандартизация или нормализация. Для этого можно обучить линейную модель, используя нормализованные данные и стандартизованные. Где качество модели будет лучше тот тип масштабирования и выбрать. Для моделей основанных на деревьях масштабирование признаков не требуется.
Необходимо снчала выполнить различные манипуляции над данными. После каждой манипуляции необходимо переобучать выбранный список моделей сначала на дефолтных данных, и следить за ростом или увеличением значений метрик.
Выводы:
Полезная информация для агроиндстрии: